{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 201 Agent\n",
    "\n",
    "## Agent \n",
    "代理的核心思想是使用语言模型来选择要执行的一系列操作。在链中，一系列操作是硬编码的（在代码中）。在代理中，语言模型用作推理引擎，以确定要执行哪些操作以及按何种顺序执行。\n",
    "\n",
    "## 概念\n",
    "这里有几个关键组件：\n",
    "\n",
    "这是负责决定下一步采取什么步骤的链。这是由语言模型和提示提供支持的。该链的输入为：\n",
    "\n",
    "工具：可用工具的说明\n",
    "用户输入：高级目标\n",
    "中间步骤：先前为实现用户输入而执行的任何（动作、工具输出）对\n",
    "\n",
    "输出是要执行的下一个操作或要发送给用户的最终响应（ AgentActions 或 AgentFinish ）。操作指定工具以及该工具的输入。\n",
    "\n",
    "不同的代理具有不同的推理提示样式、不同的输入编码方式以及解析输出的不同方式。有关内置代理的完整列表，请参阅代理类型。您还可以轻松构建自定义代理。\n",
    "\n",
    "### 工具\n",
    "工具是代理可以调用的函数。围绕工具，有两个重要的设计注意事项：\n",
    "\n",
    "为代理提供正确的工具\n",
    "以对代理最有帮助的方式描述工具\n",
    "\n",
    "如果不考虑两者，您将无法构建有效的代理。如果您不授予代理访问一组正确工具的权限，它将永远无法实现您赋予它的目标。如果你不能很好地描述这些工具，代理将不知道如何正确使用它们。\n",
    "\n",
    "LangChain提供了广泛的内置工具，但也可以很容易地定义你自己的工具（包括自定义描述）。有关内置工具的完整列表，请参阅工具集成部分\n",
    "\n",
    "### 工具包\n",
    "对于许多常见任务，代理将需要一组相关工具。为此，LangChain提供了工具包的概念 - 完成特定目标所需的大约3-5个工具组。例如，GitHub 工具包有一个用于搜索 GitHub 问题的工具、一个用于读取文件的工具、一个用于评论的工具等。\n",
    "\n",
    "### 代理执行程序\n",
    "代理执行程序是代理的运行时。这是实际调用代理，执行它选择的操作，将操作输出传递回代理，然后重复。在伪代码中，这大致如下所示：\n",
    "```python\n",
    "next_action = agent.get_action(...)\n",
    "while next_action != AgentFinish:\n",
    "    observation = run(next_action)\n",
    "    next_action = agent.get_action(..., next_action, observation)\n",
    "return next_action\n",
    "```\n",
    "虽然这看起来很简单，但此运行时会为您处理几个复杂性，包括：\n",
    "\n",
    "- 处理代理选择不存在的工具的情况\n",
    "- 工具错误情况的处理\n",
    "- 处理代理生成无法解析为工具调用的输出的情况\n",
    "- 将所有级别（代理决策、工具调用）的日志记录和可观察性记录到 stdout 和/或 LangSmith。\n",
    "\n",
    "### 定义代理\n",
    "我们首先需要创建我们的代理。这是负责确定下一步要采取的行动的链。\n",
    "\n",
    "我们将构建一个有权访问自定义工具的自定义代理。我们之所以选择这个例子，是因为对于大多数实际用例，您需要自定义代理或工具。我们将创建一个简单的工具来计算单词的长度。这很有用，因为它实际上是 LLM 可能会由于标记化而搞砸的东西。我们将首先在没有内存的情况下创建它，但随后我们将展示如何添加内存。需要内存才能启用对话。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "#设置代理\n",
    "import os\n",
    "\n",
    "os.environ['http_proxy'] = 'http://127.0.0.1:10809'\n",
    "os.environ['https_proxy'] = 'http://127.0.0.1:10809'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 首先，让我们加载将用于控制代理的语言模型。\n",
    "from langchain.chat_models import ChatOpenAI\n",
    "\n",
    "llm = ChatOpenAI(model=\"gpt-3.5-turbo\", temperature=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='There are 6 letters in the word \"educa\".')"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 我们可以看到，它很难计算字符串“educa”中的字母。\n",
    "llm.invoke(\"how many letters in the word educa?\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 接下来，让我们定义一些要使用的工具。让我们编写一个非常简单的 Python 函数来计算传入的单词的长度。\n",
    "from langchain.agents import tool\n",
    "\n",
    "\n",
    "@tool\n",
    "def get_word_length(word: str) -> int:\n",
    "    \"\"\"Returns the length of a word.\"\"\"\n",
    "    return len(word)\n",
    "\n",
    "\n",
    "tools = [get_word_length]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''\n",
    "现在让我们创建提示。由于 OpenAI 函数调用针对工具使用进行了微调，因此我们几乎不需要任何关于如何推理或如何输出格式的说明。\n",
    "我们将只有两个输入变量： input 和 agent_scratchpad . input 应为包含用户目标的字符串。\n",
    " agent_scratchpad 应该是包含先前代理工具调用和相应工具输出的消息序列。\n",
    "'''\n",
    "from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
    "\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\n",
    "            \"system\",\n",
    "            \"You are very powerful assistant, but bad at calculating lengths of words.\",\n",
    "        ),\n",
    "        (\"user\", \"{input}\"),\n",
    "        MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''\n",
    "代理如何知道它可以使用哪些工具？在本例中，我们依赖于 OpenAI 函数调用 LLM，它将函数作为单独的参数，并经过专门训练，可以知道何时调用这些函数。\n",
    "要将我们的工具传递给代理，我们只需要将它们格式化为 OpenAI 函数格式并将它们传递给我们的模型。（通过对函数进行绑定操作，我们确保每次调用模型时都会传入它们。\n",
    "'''\n",
    "from langchain.tools.render import format_tool_to_openai_function\n",
    "\n",
    "llm_with_tools = llm.bind(functions=[format_tool_to_openai_function(t) for t in tools])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "'''\n",
    "将这些部分放在一起，我们现在可以创建代理。\n",
    "我们将导入最后两个实用程序函数：一个组件用于格式化中间步骤（代理操作、工具输出对）以输入可发送到模型的消息，以及一个用于将输出消息转换为代理操作/代理完成的组件。\n",
    "'''\n",
    "from langchain.agents.format_scratchpad import format_to_openai_function_messages\n",
    "from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser\n",
    "\n",
    "agent = (\n",
    "    {\n",
    "        \"input\": lambda x: x[\"input\"],\n",
    "        \"agent_scratchpad\": lambda x: format_to_openai_function_messages(\n",
    "            x[\"intermediate_steps\"]\n",
    "        ),\n",
    "    }\n",
    "    | prompt\n",
    "    | llm_with_tools\n",
    "    | OpenAIFunctionsAgentOutputParser()\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AgentActionMessageLog(tool='get_word_length', tool_input={'word': 'educa'}, log=\"\\nInvoking: `get_word_length` with `{'word': 'educa'}`\\n\\n\\n\", message_log=[AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\\n  \"word\": \"educa\"\\n}', 'name': 'get_word_length'}})])"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 现在我们有了代理，让我们来玩吧！让我们传入一个简单的问题和空的中间步骤，看看它返回了什么：\n",
    "agent.invoke({\"input\": \"how many letters in the word educa?\", \"intermediate_steps\": []})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们可以看到它以 AgentAction to take 作为响应（它实际上是一个 AgentActionMessageLog - AgentAction 其子类也跟踪完整的消息日志）。\n",
    "### 定义运行时\n",
    "所以这只是第一步 - 现在我们需要为此编写一个运行时。\n",
    "\n",
    "最简单的方法就是不断循环，调用代理，然后执行操作，然后重复，直到返回 。 \n",
    "\n",
    "AgentFinish 让我们在下面进行编码："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TOOL NAME: get_word_length\n",
      "TOOL INPUT: {'word': 'educa'}\n",
      "There are 5 letters in the word \"educa\".\n"
     ]
    }
   ],
   "source": [
    "from langchain_core.agents import AgentFinish\n",
    "\n",
    "user_input = \"how many letters in the word educa?\"\n",
    "intermediate_steps = []\n",
    "while True:\n",
    "    output = agent.invoke(\n",
    "        {\n",
    "            \"input\": user_input,\n",
    "            \"intermediate_steps\": intermediate_steps,\n",
    "        }\n",
    "    )\n",
    "    if isinstance(output, AgentFinish):\n",
    "        final_result = output.return_values[\"output\"]\n",
    "        break\n",
    "    else:\n",
    "        print(f\"TOOL NAME: {output.tool}\")\n",
    "        print(f\"TOOL INPUT: {output.tool_input}\")\n",
    "        tool = {\"get_word_length\": get_word_length}[output.tool]\n",
    "        observation = tool.run(output.tool_input)\n",
    "        intermediate_steps.append((output, observation))\n",
    "print(final_result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 使用 AgentExecutor\n",
    "为了简化这一点，我们可以导入和使用该 AgentExecutor 类。\n",
    "\n",
    "这捆绑了上述所有内容，并增加了错误处理、提前停止、跟踪和其他生活质量改进，从而减少了您需要编写的安全措施。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.agents import AgentExecutor\n",
    "\n",
    "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "\n",
      "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
      "\u001b[32;1m\u001b[1;3m\n",
      "Invoking: `get_word_length` with `{'word': 'educa'}`\n",
      "\n",
      "\n",
      "\u001b[0m\u001b[36;1m\u001b[1;3m5\u001b[0m\u001b[32;1m\u001b[1;3mThere are 5 letters in the word \"educa\".\u001b[0m\n",
      "\n",
      "\u001b[1m> Finished chain.\u001b[0m\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'input': 'how many letters in the word educa?',\n",
       " 'output': 'There are 5 letters in the word \"educa\".'}"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "agent_executor.invoke({\"input\": \"how many letters in the word educa?\"})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 添加历史记录内存\n",
    "我们有一个代理！但是，此代理是无状态的 - 它不记得有关先前交互的任何内容。\n",
    "\n",
    "这意味着你不能轻易提出后续问题。让我们通过添加内存来解决这个问题。\n",
    "\n",
    "为了做到这一点，我们需要做两件事：\n",
    "\n",
    "在提示中为内存变量添加一个位置\n",
    "\n",
    "跟踪聊天记录\n",
    "\n",
    "首先，让我们在提示中添加一个内存位置。为此，我们为带有键 \"chat_history\" 的消息添加一个占位符。请注意，我们将其放在新用户输入的上方（以遵循对话流）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain.prompts import MessagesPlaceholder\n",
    "\n",
    "MEMORY_KEY = \"chat_history\"\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\n",
    "            \"system\",\n",
    "            \"You are very powerful assistant, but bad at calculating lengths of words.\",\n",
    "        ),\n",
    "        MessagesPlaceholder(variable_name=MEMORY_KEY),\n",
    "        (\"user\", \"{input}\"),\n",
    "        MessagesPlaceholder(variable_name=\"agent_scratchpad\"),\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 然后，我们可以设置一个列表来跟踪聊天记录\n",
    "from langchain_core.messages import AIMessage, HumanMessage\n",
    "\n",
    "chat_history = []"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 然后我们可以把它们放在一起！\n",
    "agent = (\n",
    "    {\n",
    "        \"input\": lambda x: x[\"input\"],\n",
    "        \"agent_scratchpad\": lambda x: format_to_openai_function_messages(\n",
    "            x[\"intermediate_steps\"]\n",
    "        ),\n",
    "        \"chat_history\": lambda x: x[\"chat_history\"],\n",
    "    }\n",
    "    | prompt\n",
    "    | llm_with_tools\n",
    "    | OpenAIFunctionsAgentOutputParser()\n",
    ")\n",
    "agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "\n",
      "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
      "\u001b[32;1m\u001b[1;3m\n",
      "Invoking: `get_word_length` with `{'word': 'educa'}`\n",
      "\n",
      "\n",
      "\u001b[0m\u001b[36;1m\u001b[1;3m5\u001b[0m\u001b[32;1m\u001b[1;3mThere are 5 letters in the word \"educa\".\u001b[0m\n",
      "\n",
      "\u001b[1m> Finished chain.\u001b[0m\n",
      "\n",
      "\n",
      "\u001b[1m> Entering new AgentExecutor chain...\u001b[0m\n",
      "\u001b[32;1m\u001b[1;3mNo, \"educa\" is not a real word in English.\u001b[0m\n",
      "\n",
      "\u001b[1m> Finished chain.\u001b[0m\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "{'input': 'is that a real word?',\n",
       " 'chat_history': [HumanMessage(content='how many letters in the word educa?'),\n",
       "  AIMessage(content='There are 5 letters in the word \"educa\".')],\n",
       " 'output': 'No, \"educa\" is not a real word in English.'}"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# 运行时，我们现在需要将输入和输出作为聊天记录进行跟踪\n",
    "input1 = \"how many letters in the word educa?\"\n",
    "result = agent_executor.invoke({\"input\": input1, \"chat_history\": chat_history})\n",
    "chat_history.extend(\n",
    "    [\n",
    "        HumanMessage(content=input1),\n",
    "        AIMessage(content=result[\"output\"]),\n",
    "    ]\n",
    ")\n",
    "agent_executor.invoke({\"input\": \"is that a real word?\", \"chat_history\": chat_history})"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
